package com.sky.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * redis缓存工具类;
 * 调用需要引入redis依赖,
 * 注入本对象
 */
@Component
public class RedisCacheUtil {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 尝试获取锁的方法:利用redis命令(setnx的特性)向redis中存入数据
     * 存入成功与否,表示获得锁结果
     * 参数1:存入数据(锁)的key
     * 返回值:true 获得锁; false 未获得
     */
    public boolean tryLock(String lockKey) {
        //设定10秒超时删除.避免获得锁的线程异常未成功删除,让其他线程也可以获得锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * redis添加逻辑过期字段保存
     * 参数1:redis缓存key
     * 参数2:redis缓存value
     * 参数3:逻辑过期时间
     * 参数4:时间单位
     */
    public void setRedis(String key, Object data, Long logicTime, TimeUnit timeUnit) {
        LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(timeUnit.toSeconds(logicTime));
        LogicalField logicalField = new LogicalField(localDateTime, data);
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(logicalField));
        System.out.println("存入的数据:" + logicalField);
    }

    /**
     * redis设置逻辑过期时,防止缓存击穿的查询方法
     * 参数1:数据存储的id
     * 参数2:数据对应实体类
     * 参数3:redis缓存key
     * 参数4:redis逻辑过期时间
     * 参数5:时间单位
     * 参数6:查询数据库的逻辑
     * 返回封装数据的对象
     */
    public <ID, T> T getLogicalExpiration(ID id, Class<T> type, String keyPrefix, long logicTime, TimeUnit timeUnit, Function<ID, T> databaseLogic) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        //redis中数据的key
        String key = keyPrefix+ id;
        //查询redis
        String json = opsForValue.get(key);
        if (StrUtil.isBlank(json)) {
            //如果没有数据直接返回
            System.out.println("没有数据直接返回");
            return null;
        }
        //如果查到了数据,转换成对象
        LogicalField logicalField = JSONUtil.toBean(json, LogicalField.class);
        String data = JSONUtil.toJsonStr(logicalField.getData());
        System.out.println("查到了数据:" + logicalField);
        T t = JSONUtil.toBean(data, type);
        //获取逻辑过期时间字段
        LocalDateTime LogicTime = logicalField.getLogicTime();
        //比较现在的时间判断是否过期
        if (LogicTime.isAfter(LocalDateTime.now())) {
            //如果逻辑过期时间在现在时间之后,则没有过期,直接返回商铺信息
            System.out.println("逻辑过期时间在现在时间之后,没有过期,返回商铺信息");
            return t;
        }
        //如果过期了就尝试获取锁更新Redis
        //定义锁的id
        String lockKey = "lock:key" + id;
        boolean tryLock = tryLock(lockKey);
        if (!tryLock) {
            //如果没有获取到锁,表示有人正在更新Redis了
            System.out.println("时间过期了,没有获取到锁,表示有人正在更新Redis了,返回过期的信息");
            return t;
        }
        ExecutorService FIXED_THREAD_POOL = Executors.newFixedThreadPool(1);
        //如果获取到锁了,开启新的线程去查询数据库完成更新redis操作
        FIXED_THREAD_POOL.submit(() -> {
            try {
                //查询数据库
                T dataBase = databaseLogic.apply(id);
                //更新redis中逻辑过期字段
                setRedis(key, dataBase, logicTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                System.out.println("查询数据库,成功更新redis,删除存入的锁,释放锁=====================================");
                //删除存入的数据
                stringRedisTemplate.delete(lockKey);
            }
        });
        //本身直接返回过期的信息;
        System.out.println("获取到锁了,开启新线程去执行更新redis数据的逻辑,本线程直接返回过期的信息");
        return t;
    }

    /**
     * redis设置缓存过期时间时,防止缓存穿透的查询方法
     * 参数1:数据存储的id
     * 参数2:数据对应实体类
     * 参数3:redis缓存key
     * 参数4:缓存击穿数据过期时间
     * 参数5:正常缓存过期时间
     * 参数6:时间单位
     * 参数7:查询数据库的逻辑
     * 返回封装数据的对象
     */
    public <ID, T> T getById(ID id, Class<T> type, String keyPrefix, long penetrationTime, long expirationTime, TimeUnit timeUnit, Function<ID, T> databaseLogic) {
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String key = keyPrefix + id;
        //根据商户id和前缀在redis中查询
        String json = opsForValue.get(key);
        if (StrUtil.isNotBlank(json)) {
            System.out.println("查到了数据,转换成对象返回");
            //如果查到了数据,转换成对象返回给前端
            return JSONUtil.toBean(json, type);
        }
        //穿透防护如果查到的是""值
        if ("".equals(json)) {
            System.out.println("查到了长度为0的字符串的数据,就是缓冲穿透数据直接返回");
            return null;
        }
        //redis未命中,获取自定义锁(权限)查询数据库,自定义锁key
        String lockKey = "lock:Key:" + id;
        //用id拼接锁的key调用方法存入redis
        if (!tryLock(lockKey)) {
            //没有存进去,表示数据存在已经有人存进去了,没有获得锁,不能执行查询数据库
            try {
                System.out.println("redis未命中查询数据库更新缓存,但没有获取锁,等待50毫秒重试");
                Thread.sleep(50);
                return getById(id, type, keyPrefix, penetrationTime, expirationTime, timeUnit, databaseLogic);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        // 获得锁,查询数据库
        T t = databaseLogic.apply(id);
        if (t == null) {
            //写入一个长度为0的字符串到redis,并设定有效期,使其超时剔除,避免过多占用内存
            opsForValue.set(key, "", penetrationTime, timeUnit);
            //调用方法删除锁
            stringRedisTemplate.delete(lockKey);
            System.out.println("成功获取到锁,但数据库没有该数据,保护数据库返回长度为0的字符串,并存入redis防止穿透,删除锁============");
            return null;
        }
        //把数据写入redis,并设定有效期,使其超时剔除
        opsForValue.set(key, JSONUtil.toJsonStr(t), expirationTime, timeUnit);
        //删除存入的数据
        stringRedisTemplate.delete(lockKey);
        System.out.println("数据库查询到数据,返回前端,并存入redis供后续查询,删除锁==============");
        return t;
    }
}

/**
 * redis缓存逻辑过期字段实体类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class LogicalField {
    private LocalDateTime logicTime;
    private Object data;
}